import {Geometry} from '../core/Geometry.js';
import {BufferGeometry} from '../core/BufferGeometry.js';
import {Float32BufferAttribute} from '../core/BufferAttribute.js';
import {Vector2} from '../math/Vector2.js';
import {Vector3} from '../math/Vector3.js';
import {ShapeUtils} from '../extras/ShapeUtils.js';

/**
 * Creates extruded geometry from a path shape.
 *
 * parameters = {
 *
 *  curveSegments: <int>, // number of points on the curves
 *  steps: <int>, // number of points for z-side extrusions / used for subdividing segments of extrude spline too
 *  depth: <float>, // Depth to extrude the shape
 *
 *  bevelEnabled: <bool>, // turn on bevel
 *  bevelThickness: <float>, // how deep into the original shape bevel goes
 *  bevelSize: <float>, // how far from shape outline (including bevelOffset) is bevel
 *  bevelOffset: <float>, // how far from shape outline does bevel start
 *  bevelSegments: <int>, // number of bevel layers
 *
 *  extrudePath: <THREE.Curve> // curve to extrude shape along
 *
 *  UVGenerator: <Object> // object that provides UV generator functions
 *
 * }
 */

// ExtrudeGeometry

class ExtrudeGeometry extends Geometry {

  constructor(shapes, options) {

    super();

    this.type = 'ExtrudeGeometry';

    this.parameters = {
      shapes: shapes,
      options: options
    };

    this.fromBufferGeometry(new ExtrudeBufferGeometry(shapes, options));
    this.mergeVertices();

  }

  toJSON() {

    const data = super.toJSON();

    const shapes = this.parameters.shapes;
    const options = this.parameters.options;

    return toJSON(shapes, options, data);

  }

}


// ExtrudeBufferGeometry

class ExtrudeBufferGeometry extends BufferGeometry {

  constructor(shapes, options) {

    super();

    this.type = 'ExtrudeBufferGeometry';

    this.parameters = {
      shapes: shapes,
      options: options
    };

    shapes = Array.isArray(shapes) ? shapes : [shapes];

    const scope = this;

    const verticesArray = [];
    const uvArray = [];

    for (let i = 0, l = shapes.length; i < l; i++) {

      const shape = shapes[i];
      addShape(shape);

    }

    // build geometry

    this.setAttribute('position', new Float32BufferAttribute(verticesArray, 3));
    this.setAttribute('uv', new Float32BufferAttribute(uvArray, 2));

    this.computeVertexNormals();

    // functions

    function addShape(shape) {

      const placeholder = [];

      // options

      const curveSegments = options.curveSegments !== undefined ? options.curveSegments : 12;
      const steps = options.steps !== undefined ? options.steps : 1;
      let depth = options.depth !== undefined ? options.depth : 100;

      let bevelEnabled = options.bevelEnabled !== undefined ? options.bevelEnabled : true;
      let bevelThickness = options.bevelThickness !== undefined ? options.bevelThickness : 6;
      let bevelSize = options.bevelSize !== undefined ? options.bevelSize : bevelThickness - 2;
      let bevelOffset = options.bevelOffset !== undefined ? options.bevelOffset : 0;
      let bevelSegments = options.bevelSegments !== undefined ? options.bevelSegments : 3;

      const extrudePath = options.extrudePath;

      const uvgen = options.UVGenerator !== undefined ? options.UVGenerator : WorldUVGenerator;

      // deprecated options

      if (options.amount !== undefined) {

        console.warn('THREE.ExtrudeBufferGeometry: amount has been renamed to depth.');
        depth = options.amount;

      }

      //

      let extrudePts, extrudeByPath = false;
      let splineTube, binormal, normal, position2;

      if (extrudePath) {

        extrudePts = extrudePath.getSpacedPoints(steps);

        extrudeByPath = true;
        bevelEnabled = false; // bevels not supported for path extrusion

        // SETUP TNB variables

        // TODO1 - have a .isClosed in spline?

        splineTube = extrudePath.computeFrenetFrames(steps, false);

        // console.log(splineTube, 'splineTube', splineTube.normals.length, 'steps', steps, 'extrudePts', extrudePts.length);

        binormal = new Vector3();
        normal = new Vector3();
        position2 = new Vector3();

      }

      // Safeguards if bevels are not enabled

      if (!bevelEnabled) {

        bevelSegments = 0;
        bevelThickness = 0;
        bevelSize = 0;
        bevelOffset = 0;

      }

      // Variables initialization

      const shapePoints = shape.extractPoints(curveSegments);

      let vertices = shapePoints.shape;
      const holes = shapePoints.holes;

      const reverse = !ShapeUtils.isClockWise(vertices);

      if (reverse) {

        vertices = vertices.reverse();

        // Maybe we should also check if holes are in the opposite direction, just to be safe ...

        for (let h = 0, hl = holes.length; h < hl; h++) {

          const ahole = holes[h];

          if (ShapeUtils.isClockWise(ahole)) {

            holes[h] = ahole.reverse();

          }

        }

      }


      const faces = ShapeUtils.triangulateShape(vertices, holes);

      /* Vertices */

      const contour = vertices; // vertices has all points but contour has only points of circumference

      for (let h = 0, hl = holes.length; h < hl; h++) {

        const ahole = holes[h];

        vertices = vertices.concat(ahole);

      }


      function scalePt2(pt, vec, size) {

        if (!vec) console.error("THREE.ExtrudeGeometry: vec does not exist");

        return vec.clone().multiplyScalar(size).add(pt);

      }

      const vlen = vertices.length, flen = faces.length;


      // Find directions for point movement


      function getBevelVec(inPt, inPrev, inNext) {

        // computes for inPt the corresponding point inPt' on a new contour
        //   shifted by 1 unit (length of normalized vector) to the left
        // if we walk along contour clockwise, this new contour is outside the old one
        //
        // inPt' is the intersection of the two lines parallel to the two
        //  adjacent edges of inPt at a distance of 1 unit on the left side.

        let v_trans_x, v_trans_y, shrink_by; // resulting translation vector for inPt

        // good reading for geometry algorithms (here: line-line intersection)
        // http://geomalgorithms.com/a05-_intersect-1.html

        const v_prev_x = inPt.x - inPrev.x,
          v_prev_y = inPt.y - inPrev.y;
        const v_next_x = inNext.x - inPt.x,
          v_next_y = inNext.y - inPt.y;

        const v_prev_lensq = (v_prev_x * v_prev_x + v_prev_y * v_prev_y);

        // check for collinear edges
        const collinear0 = (v_prev_x * v_next_y - v_prev_y * v_next_x);

        if (Math.abs(collinear0) > Number.EPSILON) {

          // not collinear

          // length of vectors for normalizing

          const v_prev_len = Math.sqrt(v_prev_lensq);
          const v_next_len = Math.sqrt(v_next_x * v_next_x + v_next_y * v_next_y);

          // shift adjacent points by unit vectors to the left

          const ptPrevShift_x = (inPrev.x - v_prev_y / v_prev_len);
          const ptPrevShift_y = (inPrev.y + v_prev_x / v_prev_len);

          const ptNextShift_x = (inNext.x - v_next_y / v_next_len);
          const ptNextShift_y = (inNext.y + v_next_x / v_next_len);

          // scaling factor for v_prev to intersection point

          const sf = ((ptNextShift_x - ptPrevShift_x) * v_next_y -
            (ptNextShift_y - ptPrevShift_y) * v_next_x) /
            (v_prev_x * v_next_y - v_prev_y * v_next_x);

          // vector from inPt to intersection point

          v_trans_x = (ptPrevShift_x + v_prev_x * sf - inPt.x);
          v_trans_y = (ptPrevShift_y + v_prev_y * sf - inPt.y);

          // Don't normalize!, otherwise sharp corners become ugly
          //  but prevent crazy spikes
          const v_trans_lensq = (v_trans_x * v_trans_x + v_trans_y * v_trans_y);
          if (v_trans_lensq <= 2) {

            return new Vector2(v_trans_x, v_trans_y);

          } else {

            shrink_by = Math.sqrt(v_trans_lensq / 2);

          }

        } else {

          // handle special case of collinear edges

          let direction_eq = false; // assumes: opposite

          if (v_prev_x > Number.EPSILON) {

            if (v_next_x > Number.EPSILON) {

              direction_eq = true;

            }

          } else {

            if (v_prev_x < -Number.EPSILON) {

              if (v_next_x < -Number.EPSILON) {

                direction_eq = true;

              }

            } else {

              if (Math.sign(v_prev_y) === Math.sign(v_next_y)) {

                direction_eq = true;

              }

            }

          }

          if (direction_eq) {

            // console.log("Warning: lines are a straight sequence");
            v_trans_x = -v_prev_y;
            v_trans_y = v_prev_x;
            shrink_by = Math.sqrt(v_prev_lensq);

          } else {

            // console.log("Warning: lines are a straight spike");
            v_trans_x = v_prev_x;
            v_trans_y = v_prev_y;
            shrink_by = Math.sqrt(v_prev_lensq / 2);

          }

        }

        return new Vector2(v_trans_x / shrink_by, v_trans_y / shrink_by);

      }


      const contourMovements = [];

      for (let i = 0, il = contour.length, j = il - 1, k = i + 1; i < il; i++, j++, k++) {

        if (j === il) j = 0;
        if (k === il) k = 0;

        //  (j)---(i)---(k)
        // console.log('i,j,k', i, j , k)

        contourMovements[i] = getBevelVec(contour[i], contour[j], contour[k]);

      }

      const holesMovements = [];
      let oneHoleMovements, verticesMovements = contourMovements.concat();

      for (let h = 0, hl = holes.length; h < hl; h++) {

        const ahole = holes[h];

        oneHoleMovements = [];

        for (let i = 0, il = ahole.length, j = il - 1, k = i + 1; i < il; i++, j++, k++) {

          if (j === il) j = 0;
          if (k === il) k = 0;

          //  (j)---(i)---(k)
          oneHoleMovements[i] = getBevelVec(ahole[i], ahole[j], ahole[k]);

        }

        holesMovements.push(oneHoleMovements);
        verticesMovements = verticesMovements.concat(oneHoleMovements);

      }


      // Loop bevelSegments, 1 for the front, 1 for the back

      for (let b = 0; b < bevelSegments; b++) {

        //for ( b = bevelSegments; b > 0; b -- ) {

        const t = b / bevelSegments;
        const z = bevelThickness * Math.cos(t * Math.PI / 2);
        const bs = bevelSize * Math.sin(t * Math.PI / 2) + bevelOffset;

        // contract shape

        for (let i = 0, il = contour.length; i < il; i++) {

          const vert = scalePt2(contour[i], contourMovements[i], bs);

          v(vert.x, vert.y, -z);

        }

        // expand holes

        for (let h = 0, hl = holes.length; h < hl; h++) {

          const ahole = holes[h];
          oneHoleMovements = holesMovements[h];

          for (let i = 0, il = ahole.length; i < il; i++) {

            const vert = scalePt2(ahole[i], oneHoleMovements[i], bs);

            v(vert.x, vert.y, -z);

          }

        }

      }

      const bs = bevelSize + bevelOffset;

      // Back facing vertices

      for (let i = 0; i < vlen; i++) {

        const vert = bevelEnabled ? scalePt2(vertices[i], verticesMovements[i], bs) : vertices[i];

        if (!extrudeByPath) {

          v(vert.x, vert.y, 0);

        } else {

          // v( vert.x, vert.y + extrudePts[ 0 ].y, extrudePts[ 0 ].x );

          normal.copy(splineTube.normals[0]).multiplyScalar(vert.x);
          binormal.copy(splineTube.binormals[0]).multiplyScalar(vert.y);

          position2.copy(extrudePts[0]).add(normal).add(binormal);

          v(position2.x, position2.y, position2.z);

        }

      }

      // Add stepped vertices...
      // Including front facing vertices

      for (let s = 1; s <= steps; s++) {

        for (let i = 0; i < vlen; i++) {

          const vert = bevelEnabled ? scalePt2(vertices[i], verticesMovements[i], bs) : vertices[i];

          if (!extrudeByPath) {

            v(vert.x, vert.y, depth / steps * s);

          } else {

            // v( vert.x, vert.y + extrudePts[ s - 1 ].y, extrudePts[ s - 1 ].x );

            normal.copy(splineTube.normals[s]).multiplyScalar(vert.x);
            binormal.copy(splineTube.binormals[s]).multiplyScalar(vert.y);

            position2.copy(extrudePts[s]).add(normal).add(binormal);

            v(position2.x, position2.y, position2.z);

          }

        }

      }


      // Add bevel segments planes

      //for ( b = 1; b <= bevelSegments; b ++ ) {
      for (let b = bevelSegments - 1; b >= 0; b--) {

        const t = b / bevelSegments;
        const z = bevelThickness * Math.cos(t * Math.PI / 2);
        const bs = bevelSize * Math.sin(t * Math.PI / 2) + bevelOffset;

        // contract shape

        for (let i = 0, il = contour.length; i < il; i++) {

          const vert = scalePt2(contour[i], contourMovements[i], bs);
          v(vert.x, vert.y, depth + z);

        }

        // expand holes

        for (let h = 0, hl = holes.length; h < hl; h++) {

          const ahole = holes[h];
          oneHoleMovements = holesMovements[h];

          for (let i = 0, il = ahole.length; i < il; i++) {

            const vert = scalePt2(ahole[i], oneHoleMovements[i], bs);

            if (!extrudeByPath) {

              v(vert.x, vert.y, depth + z);

            } else {

              v(vert.x, vert.y + extrudePts[steps - 1].y, extrudePts[steps - 1].x + z);

            }

          }

        }

      }

      /* Faces */

      // Top and bottom faces

      buildLidFaces();

      // Sides faces

      buildSideFaces();


      /////  Internal functions

      function buildLidFaces() {

        const start = verticesArray.length / 3;

        if (bevelEnabled) {

          let layer = 0; // steps + 1
          let offset = vlen * layer;

          // Bottom faces

          for (let i = 0; i < flen; i++) {

            const face = faces[i];
            f3(face[2] + offset, face[1] + offset, face[0] + offset);

          }

          layer = steps + bevelSegments * 2;
          offset = vlen * layer;

          // Top faces

          for (let i = 0; i < flen; i++) {

            const face = faces[i];
            f3(face[0] + offset, face[1] + offset, face[2] + offset);

          }

        } else {

          // Bottom faces

          for (let i = 0; i < flen; i++) {

            const face = faces[i];
            f3(face[2], face[1], face[0]);

          }

          // Top faces

          for (let i = 0; i < flen; i++) {

            const face = faces[i];
            f3(face[0] + vlen * steps, face[1] + vlen * steps, face[2] + vlen * steps);

          }

        }

        scope.addGroup(start, verticesArray.length / 3 - start, 0);

      }

      // Create faces for the z-sides of the shape

      function buildSideFaces() {

        const start = verticesArray.length / 3;
        let layeroffset = 0;
        sidewalls(contour, layeroffset);
        layeroffset += contour.length;

        for (let h = 0, hl = holes.length; h < hl; h++) {

          const ahole = holes[h];
          sidewalls(ahole, layeroffset);

          //, true
          layeroffset += ahole.length;

        }


        scope.addGroup(start, verticesArray.length / 3 - start, 1);


      }

      function sidewalls(contour, layeroffset) {

        let i = contour.length;

        while (--i >= 0) {

          const j = i;
          let k = i - 1;
          if (k < 0) k = contour.length - 1;

          //console.log('b', i,j, i-1, k,vertices.length);

          for (let s = 0, sl = (steps + bevelSegments * 2); s < sl; s++) {

            const slen1 = vlen * s;
            const slen2 = vlen * (s + 1);

            const a = layeroffset + j + slen1,
              b = layeroffset + k + slen1,
              c = layeroffset + k + slen2,
              d = layeroffset + j + slen2;

            f4(a, b, c, d);

          }

        }

      }

      function v(x, y, z) {

        placeholder.push(x);
        placeholder.push(y);
        placeholder.push(z);

      }


      function f3(a, b, c) {

        addVertex(a);
        addVertex(b);
        addVertex(c);

        const nextIndex = verticesArray.length / 3;
        const uvs = uvgen.generateTopUV(scope, verticesArray, nextIndex - 3, nextIndex - 2, nextIndex - 1);

        addUV(uvs[0]);
        addUV(uvs[1]);
        addUV(uvs[2]);

      }

      function f4(a, b, c, d) {

        addVertex(a);
        addVertex(b);
        addVertex(d);

        addVertex(b);
        addVertex(c);
        addVertex(d);


        const nextIndex = verticesArray.length / 3;
        const uvs = uvgen.generateSideWallUV(scope, verticesArray, nextIndex - 6, nextIndex - 3, nextIndex - 2, nextIndex - 1);

        addUV(uvs[0]);
        addUV(uvs[1]);
        addUV(uvs[3]);

        addUV(uvs[1]);
        addUV(uvs[2]);
        addUV(uvs[3]);

      }

      function addVertex(index) {

        verticesArray.push(placeholder[index * 3 + 0]);
        verticesArray.push(placeholder[index * 3 + 1]);
        verticesArray.push(placeholder[index * 3 + 2]);

      }


      function addUV(vector2) {

        uvArray.push(vector2.x);
        uvArray.push(vector2.y);

      }

    }

  }

  toJSON() {

    const data = BufferGeometry.prototype.toJSON.call(this);

    const shapes = this.parameters.shapes;
    const options = this.parameters.options;

    return toJSON(shapes, options, data);

  }

}

const WorldUVGenerator = {

  generateTopUV: function (geometry, vertices, indexA, indexB, indexC) {

    const a_x = vertices[indexA * 3];
    const a_y = vertices[indexA * 3 + 1];
    const b_x = vertices[indexB * 3];
    const b_y = vertices[indexB * 3 + 1];
    const c_x = vertices[indexC * 3];
    const c_y = vertices[indexC * 3 + 1];

    return [
      new Vector2(a_x, a_y),
      new Vector2(b_x, b_y),
      new Vector2(c_x, c_y)
    ];

  },

  generateSideWallUV: function (geometry, vertices, indexA, indexB, indexC, indexD) {

    const a_x = vertices[indexA * 3];
    const a_y = vertices[indexA * 3 + 1];
    const a_z = vertices[indexA * 3 + 2];
    const b_x = vertices[indexB * 3];
    const b_y = vertices[indexB * 3 + 1];
    const b_z = vertices[indexB * 3 + 2];
    const c_x = vertices[indexC * 3];
    const c_y = vertices[indexC * 3 + 1];
    const c_z = vertices[indexC * 3 + 2];
    const d_x = vertices[indexD * 3];
    const d_y = vertices[indexD * 3 + 1];
    const d_z = vertices[indexD * 3 + 2];

    if (Math.abs(a_y - b_y) < 0.01) {

      return [
        new Vector2(a_x, 1 - a_z),
        new Vector2(b_x, 1 - b_z),
        new Vector2(c_x, 1 - c_z),
        new Vector2(d_x, 1 - d_z)
      ];

    } else {

      return [
        new Vector2(a_y, 1 - a_z),
        new Vector2(b_y, 1 - b_z),
        new Vector2(c_y, 1 - c_z),
        new Vector2(d_y, 1 - d_z)
      ];

    }

  }
};

function toJSON(shapes, options, data) {

  data.shapes = [];

  if (Array.isArray(shapes)) {

    for (let i = 0, l = shapes.length; i < l; i++) {

      const shape = shapes[i];

      data.shapes.push(shape.uuid);

    }

  } else {

    data.shapes.push(shapes.uuid);

  }

  if (options.extrudePath !== undefined) data.options.extrudePath = options.extrudePath.toJSON();

  return data;

}


export {ExtrudeGeometry, ExtrudeBufferGeometry};
